﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Linq;
using RestSharp;
using RestSharp.Deserializers;
using RestSharp.Extensions;

namespace Freshbooks.XmlSerializers
{
    public class FreshbooksXmlDeserializer : IDeserializer
    {
        #region IDeserializer Members

        public string RootElement { get; set; }
        public string Namespace { get; set; }
        public string DateFormat { get; set; }

        public T Deserialize<T>(RestResponse response) where T : new()
        {
            if (response.Content == null)
                return default(T);

            var d = new XmlDocument();
            d.LoadXml(response.Content);
            //d.DocumentElement.Attributes.RemoveAll();
            //ClearAllAttributes(d.DocumentElement.ChildNodes);

            var sw = new StringWriter();
            var tx = new XmlTextWriter(sw);
            d.WriteTo(tx);

            response.Content = sw.ToString();


            XDocument doc = XDocument.Parse(response.Content);
            XElement root = doc.Root;
            //if (RootElement.HasValue() && doc.Root != null)
            //{                  
            //    root = doc.Root.Element(RootElement.AsNamespaced(Namespace));

            //}

            // autodetect xml namespace
            if (!Namespace.HasValue())
            {
                RemoveNamespace(doc);
            }

            var x = new T();
            Type objType = x.GetType();

            if (objType.IsSubclassOfRawGeneric(typeof(List<>)))
            {
                x = (T)HandleListDerivative(x, root, objType.Name, objType);
            }
            else
            {
                Map(x, root);
            }

            return x;
        }

        #endregion

        private void ClearAllAttributes(XmlNodeList nodes)
        {
            foreach (XmlNode node in nodes)
            {
                if (node.Attributes != null)
                    node.Attributes.RemoveAll();

                if (node.HasChildNodes)
                    ClearAllAttributes(node.ChildNodes);
            }
        }

        private void RemoveNamespace(XDocument xdoc)
        {
            foreach (XElement e in xdoc.Root.DescendantsAndSelf())
            {
                if (e.Name.Namespace != XNamespace.None)
                {
                    e.Name = XNamespace.None.GetName(e.Name.LocalName);
                }
                if (e.Attributes().Any(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None))
                {
                    e.ReplaceAttributes(
                        e.Attributes().Select(
                            a =>
                            a.IsNamespaceDeclaration
                                ? null
                                : a.Name.Namespace != XNamespace.None
                                      ? new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value)
                                      : a));
                }
            }
        }

        private void Map(object x, XElement root)
        {
            Type objType = x.GetType();
            PropertyInfo[] props = objType.GetProperties();

            foreach (PropertyInfo prop in props)
            {
                Type type = prop.PropertyType;

                if (!type.IsPublic || !prop.CanWrite)
                    continue;

                XName name = prop.Name.AsNamespaced(Namespace);
                object value = GetValueFromXml(root, name);

                if (value == null)
                {
                    // special case for inline list items
                    if (type.IsGenericType)
                    {
                        Type genericType = type.GetGenericArguments()[0];

                        XElement first = GetElementByName(root, genericType.Name);
                        if (first != null)
                        {
                            IEnumerable<XElement> elements = root.Elements(first.Name);

                            var list = (IList)Activator.CreateInstance(type);
                            PopulateListFromElements(genericType, elements, list);
                            prop.SetValue(x, list, null);
                        }
                    }
                    continue;
                }

                // check for nullable and extract underlying type
                if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    type = type.GetGenericArguments()[0];
                }

                if (type.IsPrimitive)
                {
                    prop.SetValue(x, value.ChangeType(type), null);
                }
                else if (type.IsEnum)
                {
                    object converted = Enum.Parse(type, value.ToString(), false);
                    prop.SetValue(x, converted, null);
                }
                else if (type == typeof(Uri))
                {
                    var uri = new Uri(value.ToString(), UriKind.RelativeOrAbsolute);
                    prop.SetValue(x, uri, null);
                }
                else if (type == typeof(string))
                {
                    prop.SetValue(x, value, null);
                }
                else if (type == typeof(DateTime))
                {
                    value = DateFormat.HasValue()
                                ? DateTime.ParseExact(value.ToString(), DateFormat, CultureInfo.CurrentCulture)
                                : DateTime.Parse(value.ToString());

                    prop.SetValue(x, value, null);
                }
                else if (type == typeof(Decimal))
                {
                    value = Decimal.Parse(value.ToString());
                    prop.SetValue(x, value, null);
                }
                else if (type == typeof(Guid))
                {
                    string raw = value.ToString();
                    value = string.IsNullOrEmpty(raw) ? Guid.Empty : new Guid(value.ToString());
                    prop.SetValue(x, value, null);
                }
                else if (type.IsGenericType)
                {
                    Type t = type.GetGenericArguments()[0];
                    var list = (IList)Activator.CreateInstance(type);

                    XElement container = GetElementByName(root, prop.Name.AsNamespaced(Namespace));
                    XElement first = container.Elements().FirstOrDefault();

                    IEnumerable<XElement> elements = container.Elements(first.Name);
                    PopulateListFromElements(t, elements, list);

                    prop.SetValue(x, list, null);
                }
                else if (type.IsSubclassOfRawGeneric(typeof(List<>)))
                {
                    // handles classes that derive from List<T>
                    // e.g. a collection that also has attributes
                    object list = HandleListDerivative(x, root, prop.Name, type);
                    prop.SetValue(x, list, null);
                }
                else
                {
                    // nested property classes
                    if (root != null)
                    {
                        XElement element = GetElementByName(root, name);
                        if (element != null)
                        {
                            object item = CreateAndMap(type, element);
                            prop.SetValue(x, item, null);
                        }
                    }
                }
            }
        }

        private void PopulateListFromElements(Type t, IEnumerable<XElement> elements, IList list)
        {
            foreach (XElement element in elements)
            {
                object item = CreateAndMap(t, element);
                list.Add(item);
            }
        }

        private object HandleListDerivative(object x, XElement root, string propName, Type type)
        {
            Type t = type.IsGenericType
                         ? type.GetGenericArguments()[0]
                         : type.BaseType.GetGenericArguments()[0];


            var list = (IList)Activator.CreateInstance(type);


            IEnumerable<XElement> elements = GetDescendentsByName(root, t.Name.AsNamespaced(Namespace));

            if (elements == null || !elements.Any())
            {
                var options = t.GetAttribute<FreshbooksSerializationAttribute>();
                if (options != null && !string.IsNullOrEmpty(options.AlternateName))
                    elements = root.Descendants(options.AlternateName.AsNamespaced(Namespace));
            }

            PopulateListFromElements(t, elements, list);

            // get properties too, not just list items
            // only if this isn't a generic type
            if (!type.IsGenericType)
            {
                Map(list, GetElementByName(root, propName.AsNamespaced(Namespace)));
                //Map(list, root.Element(propName.AsNamespaced(Namespace)));
            }

            return list;
        }

        private object CreateAndMap(Type t, XElement element)
        {
            object item = Activator.CreateInstance(t);
            Map(item, element);
            return item;
        }

        private object GetValueFromXml(XElement root, XName name)
        {
            object val = null;

            if (root != null)
            {
                XElement element = GetElementByName(root, name);

                if (element == null)
                {
                    XAttribute attribute = GetAttributeByName(root, name);

                    if (attribute != null)
                    {
                        val = attribute.Value;
                    }
                }
                else
                {
                    if (!element.IsEmpty || element.HasElements || element.HasAttributes)
                    {
                        if (element.Value.Trim().Length > 0)
                            val = element.Value;
                    }
                }
            }

            return val;
        }

        private XElement GetElementByName(XElement root, XName name)
        {
            XName lowerName = XName.Get(name.LocalName.ToLower(), name.NamespaceName);
            XName camelName = XName.Get(name.LocalName.ToCamelCase(CultureInfo.InvariantCulture), name.NamespaceName);
            XName dashName = XName.Get(name.LocalName.AddDashes().ToLower(), name.NamespaceName);

            if (root.Element(name) != null)
            {
                return root.Element(name);
            }

            if (root.Element(lowerName) != null)
            {
                return root.Element(lowerName);
            }

            if (root.Element(camelName) != null)
            {
                return root.Element(camelName);
            }

            if (root.Element(dashName) != null)
            {
                return root.Element(dashName);
            }

            if (name == "Value" && root.Value != null)
            {
                return root;
            }

            // try looking for element that matches sanitized property name (Order by depth)
            XElement element = root.Descendants()
                                   .OrderBy(d => d.Ancestors().Count())
                                   .FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName)
                               ?? root.Descendants()
                                      .OrderBy(d => d.Ancestors().Count())
                                      .FirstOrDefault(
                                          d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName.ToLower());

            if (element != null)
            {
                return element;
            }

            return null;
        }

        private IEnumerable<XElement> GetDescendentsByName(XElement root, XName name)
        {
            XName lowerName = XName.Get(name.LocalName.ToLower(), name.NamespaceName);
            XName camelName = XName.Get(name.LocalName.ToCamelCase(CultureInfo.InvariantCulture), name.NamespaceName);
            XName dashName = XName.Get(name.LocalName.AddDashes().ToLower(), name.NamespaceName);

            IEnumerable<XElement> descendants = root.Descendants(name);

            if (descendants != null && !descendants.Any())
            {
                descendants = root.Descendants(lowerName);
            }

            if (descendants != null && !descendants.Any())
            {
                descendants = root.Descendants(camelName);
            }

            if (descendants != null && !descendants.Any())
            {
                descendants = root.Descendants(dashName);
            }

            return descendants;
        }

        private XAttribute GetAttributeByName(XElement root, XName name)
        {
            XName lowerName = XName.Get(name.LocalName.ToLower(), name.NamespaceName);
            XName camelName = XName.Get(name.LocalName.ToCamelCase(CultureInfo.InvariantCulture), name.NamespaceName);
            XName dashName = XName.Get(name.LocalName.AddDashes().ToLower(), name.NamespaceName);

            if (root.Attribute(name) != null)
            {
                return root.Attribute(name);
            }

            if (root.Attribute(lowerName) != null)
            {
                return root.Attribute(lowerName);
            }

            if (root.Attribute(camelName) != null)
            {
                return root.Attribute(camelName);
            }

            if (root.Attribute(dashName) != null)
            {
                return root.Attribute(dashName);
            }

            // try looking for element that matches sanitized property name
            XAttribute element =
                root.Attributes().FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName);
            if (element != null)
            {
                return element;
            }

            return null;
        }
    }
}